// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Xml;

using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using Microsoft.TeamFoundation.WorkItemTracking.Proxy;

namespace Microsoft.TeamFoundation.Migration.Toolkit.Wit
{
    using MigrationFileAttachmentOperation = SyncActionContainer<IMigrationFileAttachment>;

    /// <summary>
    /// TFS update statement.
    /// </summary>
    public class TfsWorkItemUpdate : IWorkItemUpdate
    {
        private TfsCore m_core;                             // TFS core
        private Watermark m_source;                         // Source revision
        private XmlElement m_operation;                     // Operation element

        /// <summary>
        /// Creates a statement for inserting a work item.
        /// </summary>
        /// <param name="parent">Parent change list</param>
        /// <param name="rev">Fields to set</param>
        /// <returns>Update statement</returns>
        internal static TfsWorkItemUpdate CreateInsert(
            TfsWorkItemUpdatePackage parent,
            MigrationRevision rev)
        {
            XmlElement e = CreateElement(
                parent,
                "InsertWorkItem",
                rev,
                true,
                new MigrationField(CoreFieldReferenceNames.WorkItemType, parent.WorkItemType),
                new MigrationField(CoreFieldReferenceNames.CreatedDate, TfsServerDateTime.Value),
                new MigrationField(CoreFieldReferenceNames.CreatedBy, parent.Core.UserName));

            return new TfsWorkItemUpdate(parent.Core, rev.Source, e);
        }

        /// <summary>
        /// Creates a statement for updating a work item.
        /// </summary>
        /// <param name="parent">Parent change list</param>
        /// <param name="rev">Fields to set</param>
        /// <returns>Update statement</returns>
        internal static TfsWorkItemUpdate CreateUpdate(
            TfsWorkItemUpdatePackage parent,
            MigrationRevision rev)
        {
            XmlElement e = CreateElement(parent, "UpdateWorkItem", rev, false);
            // Target can be null for updates on new items (which were not created yet)
            if (parent.Target != null)
            {
                e.SetAttribute("WorkItemID", parent.Target.Id);
                e.SetAttribute("Revision", XmlConvert.ToString(parent.Target.Revision));
            }

            return new TfsWorkItemUpdate(parent.Core, rev.Source, e);
        }
        
        /// <summary>
        /// Returns revision of the original work item the update is supposed to bring.
        /// </summary>
        public int SourceRevision { get { return m_source.Revision; } }


        /// <summary>
        /// Specifies target revision for the update.
        /// </summary>
        /// <param name="watermark">Target revision</param>
        public void SetTarget(
            Watermark watermark)
        {
            if (watermark == null)
            {
                throw new ArgumentNullException("watermark");
            }
            m_operation.SetAttribute("WorkItemID", watermark.Id);
            m_operation.SetAttribute("Revision", XmlConvert.ToString(watermark.Revision));
        }

        /// <summary>
        /// Adds hyperlink to the update package
        /// </summary>
        /// <param name="uri">URI of the target item</param>
        /// <param name="comment">Link's comment</param>
        public void AddHyperlink(
            string location,
            string comment)
        {
            if (string.IsNullOrEmpty(location))
            {
                throw new ArgumentNullException("location");
            }

            XmlElement e = m_operation.OwnerDocument.CreateElement("InsertResourceLink");
            e.SetAttribute("FieldName", "System.LinkedFiles");
            e.SetAttribute("Location", location);
            if (!string.IsNullOrEmpty(comment))
            {
                e.SetAttribute("Comment", comment);
            }
            m_operation.AppendChild(e);
        }

        /// <summary>
        /// Adds external link.
        /// </summary>
        /// <param name="linkType">Link type</param>
        /// <param name="location">Location of the target item</param>
        /// <param name="comment">Comment</param>
        public void AddExternalLink(
            string linkType,
            string location,
            string comment)
        {
            if (string.IsNullOrEmpty(linkType))
            {
                throw new ArgumentNullException("linkType");
            }
            if (string.IsNullOrEmpty(location))
            {
                throw new ArgumentNullException("location");
            }
            XmlElement e = m_operation.OwnerDocument.CreateElement("InsertResourceLink");
            e.SetAttribute("FieldName", "System.BISLinks");
            e.SetAttribute("LinkType", linkType);
            e.SetAttribute("Location", location);
            if (!string.IsNullOrEmpty(comment))
            {
                e.SetAttribute("Comment", comment);
            }
            m_operation.AppendChild(e);
        }

        /// <summary>
        /// Adds a link to a work item.
        /// </summary>
        /// <param name="workItemId">Work item id</param>
        /// <param name="comment">comment</param>
        public void AddWorkItemLink(
            string workItemId,
            string comment)
        {
            if (string.IsNullOrEmpty(workItemId))
            {
                throw new ArgumentNullException("workItemId");
            }
            XmlElement e = m_operation.OwnerDocument.CreateElement("CreateRelation");
            e.SetAttribute("WorkItemID", workItemId);
            if (!string.IsNullOrEmpty(comment))
            {
                e.SetAttribute("Comment", comment);
            }
            m_operation.AppendChild(e);
        }

        /// <summary>
        /// Removes a link to the given work item.
        /// </summary>
        /// <param name="workItemId">Id of the work item</param>
        public void RemoveWorkItemLink(
            string workItemId)
        {
            if (string.IsNullOrEmpty(workItemId))
            {
                throw new ArgumentNullException("workItemId");
            }
            XmlElement e = m_operation.OwnerDocument.CreateElement("RemoveRelation");
            e.SetAttribute("WorkItemID", workItemId);
            m_operation.AppendChild(e);
        }

        /// <summary>
        /// Submits update's data into the update package.
        /// </summary>
        /// <param name="doc">XML document containing update package</param>
        internal void SubmitData(
            XmlDocument doc)
        {
            if (doc == null)
            {
                throw new ArgumentNullException("doc");
            }
            XmlElement e = (XmlElement)doc.ImportNode(m_operation, true);
            doc.DocumentElement.AppendChild(e);
        }

        // <summary>
        /// Constructor.
        /// </summary>
        /// <param name="core">TFS core</param>
        /// <param name="source">Source revision</param>
        /// <param name="e">Update instruction</param>
        private TfsWorkItemUpdate(
            TfsCore core,
            Watermark source,
            XmlElement e)
        {
            m_core = core;
            m_source = source;
            m_operation = e;
        }

        /// <summary>
        /// Creates an update package element.
        /// </summary>
        /// <param name="parent">Change list the update belongs to</param>
        /// <param name="name">Name of the element</param>
        /// <param name="rev">Revision with fields that should be set</param>
        /// <param name="setDefaultPaths">Tells whether to set default path fields</param>
        /// <param name="extraFields">Extra fields to be added to the revision</param>
        /// <returns>XML statement for the update package</returns>
        private static XmlElement CreateElement(
            TfsWorkItemUpdatePackage parent,
            string name,
            MigrationRevision rev,
            bool setDefaultPaths,
            params MigrationField[] extraFields)
        {
            XmlDocument doc = parent.Document;
            XmlElement e = doc.CreateElement(name);
            e.SetAttribute("ObjectType", "WorkItem");
            e.SetAttribute("BypassRules", "1");

            XmlElement cs = doc.CreateElement("Columns");
            e.AppendChild(cs);

            parent.Core.SaveRevision(cs, rev, parent.WorkItemType, setDefaultPaths, extraFields);

            return e;
        }

        /// <summary>
        /// Creates an xml element for inserting an attachment.
        /// Also, uploads the attachment to Tfs since that has to be 
        /// done before the Xml update is submitted.
        /// </summary>
        /// <param name="attachment">Attachment to upload.</param>
        /// <param name="clientSvc">Tfs web service reference.</param>
        /// <returns>Xml for inserting an attachment.</returns>
        private void AppendAddAttachmentElement(
            IMigrationFileAttachment attachment, 
            ClientService clientSvc)
        {
            Guid fileGuid = Guid.NewGuid();

            XmlElement e = m_operation.OwnerDocument.CreateElement("InsertFile");
            e.SetAttribute("FieldName", "System.AttachedFiles");
            e.SetAttribute("OriginalName", attachment.Name);
            e.SetAttribute("FileName", XmlConvert.ToString(fileGuid));
            e.SetAttribute("CreationDate", XmlConvert.ToString(attachment.UtcCreationDate, XmlDateTimeSerializationMode.Unspecified));
            e.SetAttribute("LastWriteDate", XmlConvert.ToString(attachment.UtcLastWriteDate, XmlDateTimeSerializationMode.Unspecified));
            e.SetAttribute("FileSize", XmlConvert.ToString(attachment.Length));
            XmlElement ec = m_operation.OwnerDocument.CreateElement("Comment");
            ec.SetAttribute("xml:space", "preserve");
            ec.InnerText = attachment.Comment;
            e.AppendChild(ec);
            m_operation.AppendChild(e);

            //Now upload the file since that has to be done before the Xml batch is executed.
            using (Stream strm = attachment.GetFileContents())
            {
                FileAttachment f = new FileAttachment();
                f.AreaNodeUri = m_core.AreaNodeUri;
                f.ProjectUri = m_core.ProjectUri;
                f.FileNameGUID = fileGuid;
                f.LocalFile = strm; // attachment.GetFileContents();

                clientSvc.UploadFile(f);
            }
            //We should be done with the attachment now
            IDisposable disp = attachment as IDisposable;
            if (disp != null)
            {
                disp.Dispose();
            }
        }

        /// <summary>
        /// Creates an xml element for deleting an attachment.
        /// </summary>
        /// <param name="attachment">Attachment to be removed.</param>
        /// <returns>Xml for deleting an attachment.</returns>
        private void AppendDeleteAttachmentElement(
            IMigrationFileAttachment attachment)
        {
            //If its being deleted, it has to be a Tfs attachment
            TfsMigrationFileAttachment a = (TfsMigrationFileAttachment)attachment;

            XmlElement e = m_operation.OwnerDocument.CreateElement("RemoveFile");
            e.SetAttribute("FileID", XmlConvert.ToString(a.FileID));
            m_operation.AppendChild(e);
        }

        /// <summary>
        /// Adds operation on the file attachment to the statement.
        /// </summary>
        /// <param name="svc">Client service</param>
        /// <param name="file">File attachment operation</param>
        internal void AddFile(
            ClientService svc,
            MigrationFileAttachmentOperation file)
        {
            if (file == null)
            {
                throw new ArgumentNullException("file");
            }
            if (file.Action == AssociationAction.Add)
            {
                AppendAddAttachmentElement(file.Item, svc);
            }
            else
            {
                Debug.Assert(file.Action == AssociationAction.Remove, "Unsupported operation!");
                AppendDeleteAttachmentElement(file.Item);
            }
        }
    }
}
